mysql IS NULL 使用索引
简介
mysql的sql查询语句中使用is null
、is not null
、!=
对索引并没有任何影响,并不会因为where
条件中使用了is null
、is not null
、!=
这些判断条件导致索引失效而全表扫描。
mysql官方文档也已经明确说明is null
并不会影响索引的使用。
MySQL can perform the same optimization on col_name IS NULL that it can use for col_name = constant_value. For example, MySQL can use indexes and ranges to search for NULL with IS NULL.
事实上,导致索引失效而全表扫描的通常是因为一次查询中回表数量太多。mysql计算认为使用索引的时间成本高于全表扫描,于是mysql宁可全表扫描也不愿意使用索引。
案例
1 | CREATE TABLE `user_info` ( |
1 | INSERT INTO `user_info` (`id`, `name`, `age`) VALUES ('1', 'tom', '18'); |
执行sql查询时使用is null
、is not null
,发现依然使用的索引查询,并没有出现索引失效的问题。
分析
分析上述现象,则需要详细了解mysql索引的工作原理以及索引数据结构。下面,分别通过工具解析和直接查看二进制文件两种方式分别分析mysql索引数据结构。
工具解析
innodb_ruby是一个非常强大的mysql分析工具,可以用来轻松解析mysql的.ibd
文件进而深入理解mysql的数据结构。
首先安装innodb_ruby
工具:
1 | yum install -y rubygems ruby-deve |
innodb_ruby
的功能很多,此处我们只需要用来解析mysql的索引结构,因此只需要如下的命令即可。更多的功能和命令详见wiki。
1 | innodb_space -s ibdata1 -T sakila/film -I PRIMARY index-recurse |
解析主键索引:
1 | $ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I PRIMARY index-recurse |
解析普通索引index_name
:
1 | $ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I index_name index-recurse |
通过解析工具数据mysql的索引结构可以发现,null
值也被储存到了索引树中,并且null
值被处理成最小的值放在index_name
索引树的最左侧。
二进制文件
找到user_info
表对应的物理文件user_info.ibd
,通过软件例如UltraEdit
打开,直接定位到第5个数据页(mysql默认一个数据页占用16KB)。
如图,这些二进制数据就是index_name
索引对应的索引页数据,只挑选其中的索引记录,展开如下:
最小记录0x00010063
1 | 01 B2 01 00 02 00 29 记录头信息 |
最大记录0x00010070
1 | 00 04 00 0B 00 00 记录头信息 |
ID为1的索引0x0001007f
1 | 03 00 00 00 10 FF F1 记录头信息 |
ID为2的索引0x0001008c
1 | 01 00 00 18 00 0B 记录头信息 |
ID为3的索引0x00010097
1 | 03 00 00 00 20 FF E8 记录头信息 |
最小记录的记录头信息最后2字节00 29
-> 0x00010063
偏移0x0029
-> 0x0001008C
,即ID为2的索引位置;
ID为2的记录头信息最后2字节00 0B
-> 0x0001008C
偏移0x000B
-> 0x00010097
,即ID为3的索引位置;
ID为3的记录头信息最后2字节FF E8
-> 0x00010097
偏移0xFFE8
-> 0x0001007F
,即ID为1的索引位置;
ID为1的记录头信息最后2字节FF F1
-> 0x0001007F
偏移0xFFF1
-> 0x00010070
,最大记录的记录位置;
由此可见索引记录是通过单向链表并以索引值排序串联在一起,而null
值被处理成最小的值放在了索引链表的最开始位置,也就是索引树的最左侧。与innodb_ruby
工具解析出来的结果一致。
误解原因
为何大众误解认为is null
、is not null
、!=
这些判断条件会导致索引失效而全表扫描呢?
导致索引失效而全表扫描的通常是因为一次查询中回表数量太多。mysql计算认为使用索引的时间成本高于全表扫描,于是mysql宁可全表扫描也不愿意使用索引。使用索引的时间成本高于全表扫描的临界值可以简单得记忆为20%左右。
详细的分析过程可以见笔者的另一篇博客:mysql回表致索引失效。
也就是如果一条查询语句导致的回表范围超过全部记录的20%,则会出现索引失效的问题。而is null
、is not null
、!=
这些判断条件经常会出现在这些回表范围很大的场景,然后被人误解为是这些判断条件导致的索引失效。
复现索引失效
复现索引失效,只需要回表范围超过全部记录的20%,如下插入1000条非null
记录。
1 | delimiter // |
此时user_info
表中一共有1003条记录,其中只有1条记录的name
值为null
。那么is null
判断语句导致的回表记录只有1/1003
不会超过临界值,而is not null
判断语句导致的回表记录有1002/1003
远远超过临界值,将出现索引失效的现象。
由下两图也可以见,is null
依然正常使用索引,而is not null
如预期由于回表率太高而宁可全表扫描也不使用索引。
使用mysql的optimizer tracing
(mysql5.6版本开始支持)功能来分析sql的执行计划:
1 | SET optimizer_trace="enabled=on"; |
optimizer tracing
输出的执行计划可见,该查询下,使用全表扫描所需要的时间成本为206.9;而使用索引所需要的时间成本为1203.4,远远高于全表扫描。因此mysql最终选择全表扫描而出现索引失效的现象。
1 | { |